Skip to main content

Game Development with TSX

Welcome to the world of Dora SSR game development! If you're a frontend developer or familiar with TypeScript and React, you'll find using TSX to write games to be an exciting and familiar experience. If you're not, don't worry—this tutorial will guide you from scratch, showing how to develop games with Dora SSR and TSX while introducing some basic TSX concepts.

1. What is TSX?

1.1 TSX Basics

TSX is a combination of TypeScript and JSX, allowing you to use HTML-like tag syntax within TypeScript to build interfaces and components. This is common in React development, and with Dora SSR, you can use TSX to define game objects and scenes. (Note: JSX is a JavaScript syntax extension that allows you to write XML-like code within JavaScript.)

Tip

To use TSX in Dora SSR, make sure to select TypeScript as the language and .tsx as the file extension when creating code files in the Web IDE.

TSX Tags and Attributes: In TSX, you can use tags and attributes just like in HTML. For example:

<sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/>
  • <sprite> is a tag representing a sprite (image) in the game.
  • file, scaleX, and scaleY are attributes that set the sprite's file path and scale.

TSX Function Components: You can create function components that accept properties and return TSX elements. This is similar to function components in React.

interface ItemProps {
x?: number;
y?: number;
children?: any;
}

const Item = (props: ItemProps) => {
return (
<node x={props.x} y={props.y}>
{/* Child elements */}
{props.children}
</node>
);
};

2. Writing Game Scenes with TSX

2.1 Creating Simple Game Objects

In Dora SSR, you can define game objects using TSX tags. For example, to create a sprite:

<sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/>

2.2 Converting to Renderable Object Instances

To convert the TSX tag above into a renderable game object, you need to use the toNode() function. This function accepts a TSX element or array of elements and returns the corresponding game node.

import { React, toNode } from 'DoraX';

const node = toNode(<sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/>);

Now, node is an instantiated game scene node object.

3. Creating TSX Function Components

Function components make your code more reusable and readable. Here's how to create a simple box component in Dora SSR.

import { React, toNode } from 'DoraX';

interface BoxProps {
x?: number;
y?: number;
color?: number;
}

const Box = (props: BoxProps) => {
return (
<draw-node x={props.x ?? 0} y={props.y ?? 0}>
<rect-shape width={100} height={100} fillColor={props.color || 0xffffffff}/>
</draw-node>
);
};

In the example above, Box is a function component that accepts x, y, and color properties and returns a draw node with a rectangle shape.

Using the component:

const boxes = [
<Box x={0} y={0} color={0xffff0000}/>,
<Box x={150} y={0} color={0xff00ff00}/>,
<Box x={300} y={0} color={0xff0000ff}/>,
];

const scene = toNode(boxes);

In the example above, we create three boxes of different colors and place them in an array, which we then pass to the toNode() function to instantiate as game scene nodes.

4. Using useRef to Access Instantiated Objects

In game development, you might need to directly manipulate a game object, such as changing its position, rotating it, or responding to events. The useRef() function helps you access the instantiated object for a TSX element.

import { React, useRef, toNode } from 'DoraX';
import { Body, BodyMoveType, Vec2 } from 'Dora';

const boxRef = useRef<Body.Type>();

const MovableBox = () => {
return (
<body ref={boxRef} x={0} y={0} type={BodyMoveType.Dynamic}>
<rect-fixture width={100} height={100}/>
<draw-node>
<rect-shape width={100} height={100} fillColor={0xffffffff}/>
</draw-node>
</body>
);
};

const scene = toNode(
<physics-world>
<MovableBox/>
</physics-world>
);

// Now, you can manipulate the instantiated object via boxRef.current
if (boxRef.current) {
boxRef.current.position = Vec2(200, 200);
}

In the example above, boxRef is a reference to the instantiated object of the <body> tag. You can access and manipulate this object through boxRef.current.

5. Creating Class Components

In addition to function components, you can also create class-based components. Class components can hold state and lifecycle methods, allowing more flexible management of component behavior. However, class components are relatively more complex, so function components are generally recommended.

import { React, toNode, useRef } from 'DoraX';
import { Label } from 'Dora';

// Define initial properties for the Counter component
interface CounterProps {
count: number;
}

// Create a Counter component by extending React.Component
class Counter extends React.Component<CounterProps> {
count: number;
labelRef: JSX.Ref<Label.Type>;

// Constructor to accept initial properties
constructor(props: CounterProps) {
super(props);
this.count = props.count;
this.labelRef = useRef<Label.Type>();
}

// Render function to return TSX elements
render() {
return (
<label ref={this.labelRef} text={this.count.toString()}
fontName='sarasa-mono-sc-regular' fontSize={80}
onTapped={this.onTapped}/>
);
}

// Tap event handler
onTapped = () => {
if (this.labelRef.current) {
this.labelRef.current.text = (++this.count).toString();
}
};
}

// Instantiate the Counter component
toNode(<Counter count={1}/>);

In the example above, we create a counter component Counter that holds a count value and a label. When the label is tapped, the count increases, and the label text is updated.

6. Complete Example: Creating a Simple Game

Let's put everything together and create a simple game that includes:

  • A movable character (sprite)
  • Some static obstacles (boxes)
  • Tap the screen to control the character's movement

6.1 Define the Player Component

import { React, useRef, toNode } from 'DoraX';
import { Body, BodyMoveType, Vec2 } from 'Dora';

const playerRef = useRef<Body.Type>();

const Player = () => {
return (
<body ref={playerRef} x={0} y={0} type={BodyMoveType.Dynamic}
linearAcceleration={Vec2.zero} linearDamping={1}>
<rect-fixture width={50} height={50}/>
<draw-node>
<rect-shape width={50} height={50} fillColor={0xff00ff00}/>
</draw-node>
</body>
);
};

In the code above, Player is a player component with a movable rectangle body. playerRef is a reference that points to the instantiated player object.

6.2 Define the Obstacle Component

const Obstacle = (props: {x: number; y: number}) => {
return (
<body type={BodyMoveType.Static} x={props.x} y={props.y}>
<rect-fixture width={100} height={100}/>
<draw-node>
<rect-shape width={100} height={100} fillColor={0xffff0000}/>
</draw-node>
</body>
);
};

Obstacle is an obstacle component that consists of a static rectangular body. You can set the position of the obstacle by passing x and y as properties.

6.3 Create the Game Scene

const GameScene = () => {
return (
<physics-world
onTapBegan={touch => {
// Control player movement to the tap location
const {current: player} = playerRef;
if (player) {
player.velocity = touch.location
.sub(player.position)
.normalize()
.mul(300);
}
}}>
<Player/>
<Obstacle x={200} y={0}/>
<Obstacle x={-200} y={0}/>
</physics-world>
);
};

In the GameScene component, we create a physics world, listen for tap events, and control the player's movement toward the tap location. We also add two obstacles. Note that all physics bodies must be child nodes inside the physics-world component.

6.4 Run the Game

Instantiate the scene node:

const scene = toNode(<GameScene/>);

Now, you've created a simple game where the character can move on the screen and interact with obstacles.

7. Some Special TSX Elements

In Dora SSR, there are some special TSX elements that provide functionality beyond creating scene nodes.

7.1 Action Elements

Action elements are used to perform a series of actions, such as moving, rotating, scaling, etc. You can use action elements in TSX and add them as child elements to game objects.

<sprite file="Image/logo.png">
<move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/>
</sprite>

In the example above, we create a sprite and add a move action to make it move from (0, 0) to (200, 200). This animation will automatically play when the parent node is created.

<sprite file="Image/logo.png">
<loop>
<move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/>
<move time={0.5} startX={200} startY={200} stopX={0} stopY={0}/>
</loop>
</sprite>

A looping action can be created using the <loop> tag, which allows the action sequence of child elements to loop continuously. If you want to create an action sequence that plays only once, you can use the <sequence> tag instead. For simultaneous playback of multiple actions, you can use the <spawn> tag in combination. Note that <loop>, <sequence>, and <spawn> tags can only contain action elements, and the <loop> tag can only be used as the outermost nested tag, while <sequence> and <spawn> tags can be combined freely.

If you want to create an action sequence to be used later, you can wrap it with an <action> tag at the outermost layer.

import { ActionDef, Sprite } from "Dora";
import { React, toNode, useRef } from "DoraX";

// Create a function component containing a sprite and action sequence
const ActionNode = () => {
// Create references
const spriteRef = useRef<Sprite.Type>();
const actionRef = useRef<ActionDef.Type>();

// Play the action sequence in the click event handler
const onTapped = () => {
const {current: sprite} = spriteRef;
const {current: action} = actionRef;
if (sprite && action && sprite.actionCount === 0) {
sprite.perform(action);
}
};

// Return the sprite and action sequence to be used later
return (
<sprite ref={spriteRef} file="Image/logo.png" onTapped={onTapped}>
<action ref={actionRef}>
<sequence>
<move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/>
<move time={0.5} startX={200} startY={200} stopX={0} stopY={0}/>
</sequence>
</action>
</sprite>
);
};

// Instantiate the component
toNode(<ActionNode/>);

In the example above, we create a component ActionNode containing a sprite and an action sequence, which plays the action sequence when tapped.

Here’s the English version of your MDX content:

7.2 Currently Supported Action Elements

Currently, Dora SSR supports the following action elements:

  • <action>: Creates a referenceable action sequence.
  • <anchor-x>: Continuously changes the X anchor point of the node.
  • <anchor-y>: Continuously changes the Y anchor point of the node.
  • <angle>: Continuously changes the angle of the node (Z-axis).
  • <angle-x>: Continuously changes the rotation angle on the X-axis.
  • <angle-y>: Continuously changes the rotation angle on the Y-axis.
  • <delay>: Adds a delay in the animation timeline.
  • <event>: Triggers an event.
  • <width>: Continuously changes the width of the node.
  • <height>: Continuously changes the height of the node.
  • <hide>: Hides the node.
  • <show>: Shows the node.
  • <move>: Continuously changes the node's X and Y position.
  • <move-x>: Continuously changes the node's X position.
  • <move-y>: Continuously changes the node's Y position.
  • <move-z>: Continuously changes the node's Z position.
  • <opacity>: Continuously changes the opacity of the node.
  • <roll>: Continuously changes the rotation of the node.
  • <scale>: Continuously changes the scale of the node on both X and Y axes.
  • <scale-x>: Continuously changes the scale of the node on the X-axis.
  • <scale-y>: Continuously changes the scale of the node on the Y-axis.
  • <skew-x>: Continuously changes the skew on the X-axis.
  • <skew-y>: Continuously changes the skew on the Y-axis.
  • <frame>: Creates a frame animation.
  • <loop>: Repeats an action.
  • <spawn>: Executes a group of actions in parallel.
  • <sequence>: Executes a series of actions sequentially.

7.3 Descriptive Elements

In addition to action elements, Dora SSR provides descriptive elements that further describe the appearance and behavior of the game objects to be created.

<physics-world>
<contact groupA={0} groupB={1} enabled={false}/> {/* Defines the physical group collision relationship */}

<body type={BodyMoveType.Dynamic} group={0}>
<disk-fixture radius={50}/> {/* Defines the collision shape of the physical body */}
<draw-node>
<dot-shape radius={50}/> {/* Defines the shape of the draw node */}
</draw-node>
</body>

<effek-node>
<effek file='Particle/effek/Laser01.efk'/> {/* Defines the information for playing effects */}
</effek-node>
</physics-world>

In the example above, we use several descriptive elements, including <contact>, <disk-fixture>, <dot-shape>, and <effek>. These elements are not instantiated as independent game objects but are used to complement the description of the physical properties, collision shapes, draw shapes, and effect information for the parent game objects to be created.

7.4 Currently Supported Descriptive Elements

Currently, Dora SSR supports the following descriptive elements:

  • Draw shape elements usable under <draw-node>:

    • <dot-shape>: Draws a dot or a filled circle.
    • <segment-shape>: Draws a line segment.
    • <polygon-shape>: Draws a polygon.
    • <rect-shape>: Draws a rectangle.
    • <verts-shape>: Draws a polygon where each vertex has its own color.
  • Collision shape elements usable under <body>:

    • <rect-fixture>: Defines a rectangular collision body.
    • <polygon-fixture>: Defines a polygonal collision body.
    • <multi-fixture>: Defines a concave collision body composed of multiple convex polygons.
    • <disk-fixture>: Defines a circular collision body.
    • <chain-fixture>: Defines a chain-shaped collision body composed of line segments.

7.5 Using <custom-node> to Create Custom Nodes

If you need to create a custom game node, you can use the <custom-node> element. This element allows you to reuse program modules written in code other than TSX to create other game objects. You can encapsulate this code as a new TSX component.

// Import a button component written in non-TSX code
import * as ButtonCreate from 'UI/Control/Basic/Button';
import { Button } from 'UI/Control/Basic/Button';

// Define properties for the new TSX button component
interface ButtonProps {
ref?: JSX.Ref<Button.Type>;
text: string;
width: number;
height: number;
onClick?: () => void;
}

// Create a new TSX button component using `<custom-node>`
// and reuse the code of the imported external component
const Button = (props: ButtonProps) => {
return <custom-node onCreate={() => {
const button = ButtonCreate({
text: props.text,
width: props.width,
height: props.height
});
button.onTapped(() => {
if (props.onClick) {
props.onClick();
}
});
if (props.ref) {
(props.ref.current as any) = button;
}
return button;
}}/>;
};

// Use the new TSX button component
toNode(
<Button text="Button" width={60} height={60}/>
);

In the example above, we create a new TSX button component using the <custom-node> element, reusing the code of an externally imported button component written in non-TSX code. This way, you can use custom game nodes in Dora SSR, extending the functionality of the TSX language to create game objects.

7.6 Using <custom-element> to Create Custom Elements

If you need to create descriptive data using TSX syntax and implement the parsing, instantiation, or rendering of this descriptive data yourself, you can use the <custom-element> element. This element allows you to customize the creation and processing logic of game objects, enabling more flexible framework extensions.

// Define a custom element Item
interface ItemProps {
value: number;
}
const Item = (props: ItemProps) => {
return <custom-element name='Item' data={props}/>;
};

// Define a custom element List
interface ListProps {
children?: any | any[];
}
const List = (props: ListProps) => {
return <custom-element name='List' data={props}/>;
};

// Create custom JSX descriptive data
const jsxObject = (
<List>
<Item value={0}/>
<Item value={1}/>
</List>
);

// Print the created JSX descriptive data
p(jsxObject);

The output of jsxObject is as follows:

{
[type] = "custom-element"
[children] = {
}
[props] = {
[name] = "List"
[data] = {
[children] = {
[1] = {
[type] = "custom-element"
[children] = {
}
[props] = {
[name] = "Item"
[data] = {
[value] = 0
[children] = {
}
}
}
}
[2] = {
[type] = "custom-element"
[children] = {
}
[props] = {
[name] = "Item"
[data] = {
[value] = 1
[children] = {
}
}
}
}
}
}
}
}

In the example above, we create two custom elements Item and List using the <custom-element> element and create JSX descriptive data containing these two custom elements. You can continue to write programs to access this descriptive data object and implement custom object creation and processing logic.

8. Conclusion

In this tutorial, we covered how to develop games using Dora SSR and TSX, including:

  • TSX basics: tags, attributes, and function components
  • Using toNode() to convert TSX tags into renderable object instances
  • Using useRef to access instantiated game objects
  • Creating custom game components
  • Combining components to build a complete game scene

Dora SSR provides a familiar yet powerful platform for frontend developers, allowing you to easily apply your existing TypeScript and TSX knowledge to game development. Now, try extending this example by adding more game elements and logic, and explore more of Dora SSR's features!

Appendix: Key Functions and Types

Here are some important functions and types used in Dora SSR:

  • React.createElement: Used to create TSX elements. Typically, this doesn't need to be used directly as it's called automatically by the engine.
  • toNode(enode): Converts a TSX element or array of elements into game nodes.
  • useRef<T>(item?: T): Creates a reference to access instantiated objects.
  • preloadAsync(enode, handler?): Asynchronously preloads the art assets required by a node.

Using these functions will help you efficiently write game logic and interfaces in Dora SSR. I hope this tutorial has been helpful, and I wish you success in your game development journey!